cli-mergeguard 7.0.8 → 7.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/installer.mjs +123 -6
- package/package.json +1 -1
- package/skill/SKILL.md +92 -49
- package/skill/skill.json +1 -1
package/installer.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 八个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
|
|
3
3
|
* 禁止第二套超时、第二套版本来源、第二套 bin 名。
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import { constants, existsSync, readFileSync } from 'node:fs'
|
|
7
|
+
import { cp, lstat, mkdir, open, rm, writeFile } from 'node:fs/promises'
|
|
7
8
|
import { dirname, join, resolve } from 'node:path'
|
|
8
9
|
import { stdin, stdout } from 'node:process'
|
|
9
10
|
import { createInterface } from 'node:readline/promises'
|
|
@@ -12,6 +13,18 @@ import { fileURLToPath } from 'node:url'
|
|
|
12
13
|
export const LOOKUP_TIMEOUT_MS = 8000
|
|
13
14
|
export const CALL_TIMEOUT_MS = 120_000
|
|
14
15
|
const INSTALL_META = 'install-meta.json'
|
|
16
|
+
const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
|
|
17
|
+
const BRAIN_CLIENT_TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
|
|
18
|
+
const BRAIN_CLIENT_TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
|
|
19
|
+
const BRAIN_CLIENT_AUTH_SCHEME = 'BrainClient'
|
|
20
|
+
const BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES = 16_384
|
|
21
|
+
const BRAIN_CLIENT_TOKEN_FILE_MODE = 0o600
|
|
22
|
+
const FEEDBACK_COMMENT_MAX = 500
|
|
23
|
+
const FEEDBACK_SCORE_MIN = 0
|
|
24
|
+
const FEEDBACK_SCORE_MAX = 100
|
|
25
|
+
const BRAIN_CLIENT_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
|
|
26
|
+
const FEEDBACK_INVOCATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
27
|
+
const FEEDBACK_SCORE_PATTERN = /^(?:0|[1-9]\d{0,2})$/
|
|
15
28
|
|
|
16
29
|
function asObject(value, label) {
|
|
17
30
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -112,12 +125,113 @@ export async function callOfficialSkill(context, operation, input) {
|
|
|
112
125
|
return payload
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
export function feedbackCommandInput(args) {
|
|
129
|
+
const invocationId = requiredString(args[1], 'feedback invocation id')
|
|
130
|
+
if (!FEEDBACK_INVOCATION_PATTERN.test(invocationId)) {
|
|
131
|
+
throw new Error('feedback invocation id must be the UUID returned by a real skill response')
|
|
132
|
+
}
|
|
133
|
+
const scoreText = requiredString(args[2], 'feedback score')
|
|
134
|
+
if (!FEEDBACK_SCORE_PATTERN.test(scoreText)) {
|
|
135
|
+
throw new Error(`feedback score must be an integer between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
|
|
136
|
+
}
|
|
137
|
+
const score = Number(scoreText)
|
|
138
|
+
if (!Number.isInteger(score) || score < FEEDBACK_SCORE_MIN || score > FEEDBACK_SCORE_MAX) {
|
|
139
|
+
throw new Error(`feedback score must be between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
|
|
140
|
+
}
|
|
141
|
+
const userComment = args.slice(3).join(' ').trim()
|
|
142
|
+
if (!userComment) throw new Error('feedback comment is required')
|
|
143
|
+
if (userComment.length > FEEDBACK_COMMENT_MAX) {
|
|
144
|
+
throw new Error(`feedback comment must be at most ${FEEDBACK_COMMENT_MAX} characters`)
|
|
145
|
+
}
|
|
146
|
+
return { invocationId, score, userComment }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function brainClientAuthorization(context, environment) {
|
|
150
|
+
const configuredPath = typeof environment[BRAIN_CLIENT_TOKEN_FILE_ENV] === 'string'
|
|
151
|
+
? environment[BRAIN_CLIENT_TOKEN_FILE_ENV].trim() : ''
|
|
152
|
+
if (!configuredPath) throw new Error(`${BRAIN_CLIENT_TOKEN_FILE_ENV} is required`)
|
|
153
|
+
if (process.platform === 'win32' || typeof process.getuid !== 'function') {
|
|
154
|
+
throw new Error('Brain Client token file ownership cannot be verified')
|
|
155
|
+
}
|
|
156
|
+
const tokenFilePath = resolve(configuredPath)
|
|
157
|
+
const linkStatus = await lstat(tokenFilePath)
|
|
158
|
+
if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
|
|
159
|
+
const handle = await open(tokenFilePath, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
160
|
+
try {
|
|
161
|
+
const status = await handle.stat()
|
|
162
|
+
if (!status.isFile() || status.uid !== process.getuid()
|
|
163
|
+
|| (status.mode & 0o777) !== BRAIN_CLIENT_TOKEN_FILE_MODE
|
|
164
|
+
|| status.size < 1 || status.size > BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES) {
|
|
165
|
+
throw new Error('Brain Client token file must be owned by the current user with mode 0600')
|
|
166
|
+
}
|
|
167
|
+
const tokenFile = asObject(JSON.parse(await handle.readFile('utf8')), 'Brain Client token file')
|
|
168
|
+
const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
|
|
169
|
+
if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
|
|
170
|
+
throw new Error('Brain Client token file contains unknown or missing fields')
|
|
171
|
+
}
|
|
172
|
+
const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
|
|
173
|
+
if (tokenFile.schemaVersion !== BRAIN_CLIENT_TOKEN_FILE_VERSION
|
|
174
|
+
|| tokenFile.authorizationScheme !== BRAIN_CLIENT_AUTH_SCHEME
|
|
175
|
+
|| endpoint.origin !== new URL(context.endpoint).origin
|
|
176
|
+
|| endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
|
|
177
|
+
|| endpoint.username || endpoint.password
|
|
178
|
+
|| !BRAIN_CLIENT_TOKEN_PATTERN.test(tokenFile.token)) {
|
|
179
|
+
throw new Error('Brain Client token file authority is invalid')
|
|
180
|
+
}
|
|
181
|
+
return `${BRAIN_CLIENT_AUTH_SCHEME} ${tokenFile.token}`
|
|
182
|
+
} finally {
|
|
183
|
+
await handle.close()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function submitOfficialSkillFeedback(context, args, environment, request) {
|
|
188
|
+
const input = feedbackCommandInput(args)
|
|
189
|
+
const authorization = await brainClientAuthorization(context, environment)
|
|
190
|
+
const requestId = `${context.runtimeCode}-${randomUUID()}`
|
|
191
|
+
let response
|
|
192
|
+
try {
|
|
193
|
+
response = await request(new URL(FEEDBACK_API_PATH, context.endpoint), {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
Authorization: authorization,
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
requestId,
|
|
201
|
+
skillId: context.runtimeCode,
|
|
202
|
+
invocationId: input.invocationId,
|
|
203
|
+
score: input.score,
|
|
204
|
+
userComment: input.userComment,
|
|
205
|
+
}),
|
|
206
|
+
signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
|
|
207
|
+
})
|
|
208
|
+
} catch {
|
|
209
|
+
throw new Error('cli.tax feedback request failed')
|
|
210
|
+
}
|
|
211
|
+
let payload
|
|
212
|
+
try {
|
|
213
|
+
payload = asObject(await response.json(), 'cli.tax feedback response')
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof Error && error.message.startsWith('cli.tax feedback response')) throw error
|
|
216
|
+
throw new Error(`cli.tax feedback failed: non-JSON response (HTTP ${response.status})`)
|
|
217
|
+
}
|
|
218
|
+
if (!response.ok || payload.ok !== true) {
|
|
219
|
+
throw new Error(`cli.tax feedback failed: HTTP ${response.status}`)
|
|
220
|
+
}
|
|
221
|
+
if (payload.requestId !== requestId || typeof payload.id !== 'string'
|
|
222
|
+
|| !FEEDBACK_INVOCATION_PATTERN.test(payload.id)
|
|
223
|
+
|| typeof payload.duplicated !== 'boolean') {
|
|
224
|
+
throw new Error('cli.tax feedback response authority is invalid')
|
|
225
|
+
}
|
|
226
|
+
return { id: payload.id, requestId, duplicated: payload.duplicated }
|
|
227
|
+
}
|
|
228
|
+
|
|
115
229
|
export async function installOfficialSkill(context, explicit) {
|
|
116
230
|
const target = installTarget(context.skillName, explicit)
|
|
117
231
|
await mkdir(target, { recursive: true })
|
|
118
232
|
const previous = readInstallMeta(target)
|
|
119
|
-
await
|
|
120
|
-
await
|
|
233
|
+
await rm(join(target, 'references'), { recursive: true, force: true })
|
|
234
|
+
await cp(context.skillDir, target, { recursive: true, force: true })
|
|
121
235
|
const installed = asObject(JSON.parse(readFileSync(join(target, 'skill.json'), 'utf8')), 'installed skill.json')
|
|
122
236
|
const installedVersion = requiredString(installed.version, 'installed skill.json version')
|
|
123
237
|
await writeFile(join(target, INSTALL_META), `${JSON.stringify({
|
|
@@ -170,7 +284,6 @@ export function defaultUsage(context, extraLines) {
|
|
|
170
284
|
' Check whether the installed skill has a newer version.',
|
|
171
285
|
` npx ${context.npmName}@latest run`,
|
|
172
286
|
' Run the skill handshake: discover capabilities and collect intake answers.',
|
|
173
|
-
'',
|
|
174
287
|
`Endpoint: ${context.endpoint}`,
|
|
175
288
|
]
|
|
176
289
|
if (extraLines?.length) lines.push('', ...extraLines)
|
|
@@ -227,6 +340,10 @@ export async function dispatchOfficialSkillCli(options) {
|
|
|
227
340
|
if (command === 'install') await installOfficialSkill(context, argument)
|
|
228
341
|
else if (command === 'check') await checkOfficialSkill(context, argument)
|
|
229
342
|
else if (command === 'run') await options.runCommand(context)
|
|
343
|
+
else if (command === 'feedback') {
|
|
344
|
+
const receipt = await submitOfficialSkillFeedback(context, args, process.env, fetch)
|
|
345
|
+
console.log(`${context.displayName} feedback accepted: ${receipt.id}`)
|
|
346
|
+
}
|
|
230
347
|
else if (command === 'help' || command === '--help' || command === '-h') {
|
|
231
348
|
console.log(options.usage ? options.usage(context) : defaultUsage(context, options.extraUsageLines))
|
|
232
349
|
} else {
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -5,67 +5,110 @@ description: '智能合并守卫:快照分支+预演+验证式合并+规则衰
|
|
|
5
5
|
|
|
6
6
|
# MergeGuard
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Package version: v7.0.18
|
|
9
|
+
|
|
10
|
+
当前实现是“规则编译与守门协议”,不是可直接操作 git 的合并器。任何仓库写入、快照、合并或回滚都必须交给真实 local runner;远程纯运行时一律 fail-closed。
|
|
11
|
+
|
|
12
|
+
## M1–M5 能力状态
|
|
13
|
+
|
|
14
|
+
| 编号 | 状态 | 当前边界 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| M1 完整 JSON Schema | 已实现 | `capabilities.operationSchemas` 返回结构化 JSON Schema,不返回伪类型字符串 |
|
|
17
|
+
| M2 RuleGuard DSL | 已实现(regex) | 版本化 regex 规则、编译校验、显式审计豁免;AST 规则仍规划中 |
|
|
18
|
+
| M3 L2 结构合并协议 | 规划中 | AST、JSON 键路径、公式图合并都不得声称已执行 |
|
|
19
|
+
| M4 git 映射 | 规划中 / local runner required | 当前无真实仓库、分支、worktree、commit、持久快照或台账 |
|
|
20
|
+
| M5 Validator 复用 | 已实现(验签桥) | 可验签 `cli.tax.test-evidence/1.0`;实际 merge/rollback 即使证据有效仍需 local runner |
|
|
21
|
+
|
|
22
|
+
## 操作能力矩阵
|
|
23
|
+
|
|
24
|
+
| 类别 | 操作 | 行为 |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| 纯操作 | capabilities、help、intake、resolve-propose、ruleguard-compile、ruleguard-scan | 只计算或生成提案,不写仓库 |
|
|
27
|
+
| local runner required | branch-create/list/switch、diff-report、preflight、merge-verified、rollback、ledger-query | 返回 `blocked + LOCAL-RUNNER-REQUIRED`,绝不返回 merged/rolled-back |
|
|
28
|
+
| planned | L2 AST/JSON/公式图、持久快照、持久台账、真实 git mapping | 仅在 capability matrix 标记,不作为可调用成功能力 |
|
|
29
|
+
|
|
30
|
+
调用前必须先执行 `capabilities`。调用方必须按 `operationStatus` 判断边界,不能把 operation 名称等同于已经实现。
|
|
31
|
+
|
|
32
|
+
## RuleGuard regex DSL
|
|
33
|
+
|
|
34
|
+
规则 schema 为 `mergeguard.ruleguard-rule/1.0`,必须包含:
|
|
35
|
+
|
|
36
|
+
- 稳定 id、`engine: regex`、pattern、合法 flags;
|
|
37
|
+
- P0/P1/P2 severity、message、fix、规则 version;
|
|
38
|
+
- 显式 exemptions 数组,即使为空也必须出现。
|
|
39
|
+
|
|
40
|
+
规则集 schema 为 `mergeguard.ruleguard-ruleset/1.0`,必须声明独立 ruleset version 和 `engine: regex`。`engine: ast` 会确定性返回 `RULEGUARD-AST-PLANNED`,不会偷偷按 regex 执行。
|
|
41
|
+
|
|
42
|
+
每个豁免必须包含 exemptionId、pathPattern、reason、approvedBy、ticket,可选 expiresAt。命中有效豁免时扫描结果必须返回 `exemptionAudit`,记录规则/规则集版本、文件、批准人、工单和原因;过期豁免不生效。无审计字段的“白名单”禁止使用。
|
|
43
|
+
|
|
44
|
+
示例:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"schemaVersion": "mergeguard.ruleguard-rule/1.0",
|
|
49
|
+
"id": "no-console",
|
|
50
|
+
"engine": "regex",
|
|
51
|
+
"pattern": "console\\.log\\(",
|
|
52
|
+
"flags": "g",
|
|
53
|
+
"severity": "P1",
|
|
54
|
+
"message": "console.log is forbidden",
|
|
55
|
+
"fix": "Use the audited logger",
|
|
56
|
+
"version": "v1.0.0",
|
|
57
|
+
"exemptions": [{
|
|
58
|
+
"exemptionId": "legacy-console",
|
|
59
|
+
"pathPattern": "^src/legacy\\.ts$",
|
|
60
|
+
"reason": "Temporary migration observability",
|
|
61
|
+
"approvedBy": "security-owner",
|
|
62
|
+
"ticket": "SEC-42",
|
|
63
|
+
"expiresAt": "2026-09-30T00:00:00.000Z"
|
|
64
|
+
}]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
9
67
|
|
|
10
|
-
##
|
|
68
|
+
## 合并与回滚的强制边界
|
|
11
69
|
|
|
12
|
-
|
|
13
|
-
2. **AI 提议、测试裁决**:冲突方案可由模型生成,但必须通过测试验证才算数
|
|
14
|
-
3. **小白可用**:零 git 心智的"快照分支"模式,全程向导式
|
|
70
|
+
远程 runtime 没有仓库文件系统和持久状态,因此:
|
|
15
71
|
|
|
16
|
-
|
|
72
|
+
1. branch 操作不能创建或切换真实分支;
|
|
73
|
+
2. preflight 不能声称读取真实 base/ours/theirs;
|
|
74
|
+
3. merge-verified 不会修改目标,也不会生成伪 snapshotId;
|
|
75
|
+
4. rollback 不会返回 `rolled-back`;
|
|
76
|
+
5. ledger-query 不会返回内存伪台账。
|
|
17
77
|
|
|
18
|
-
|
|
19
|
-
- **L2 结构层**:AST 级/JSON 键路径级/公式图节点级合并(主力)
|
|
20
|
-
- **L3 意图层**:AI 分析冲突意图 + 生成分辨率提案
|
|
78
|
+
local runner 后续实现必须提供仓库 identity、基线 commit、隔离目录、写前不可变快照 receipt、实际 git 命令映射、原子落盘/回滚结果以及持久审计记录。任何一项缺失都要 blocked。
|
|
21
79
|
|
|
22
|
-
|
|
80
|
+
若执行链启用了 ArchGuard,进入 preflight 前必须读取最后一条 checkpoint 台账并核对 contract digest;漂移灯不是 green、台账缺失或摘要不一致时必须 blocked。MergeGuard 不修改架构合同,也不把 ArchGuard 的块级回滚替换成分支合并回滚。
|
|
23
81
|
|
|
24
|
-
|
|
25
|
-
|------|------|
|
|
26
|
-
| capabilities / help | 能力发现 + JSON Schema |
|
|
27
|
-
| intake | 收集仓库形态/风险等级/基线 |
|
|
28
|
-
| branch-create | 创建快照分支 |
|
|
29
|
-
| branch-list | 列出所有分支 |
|
|
30
|
-
| branch-switch | 切换当前分支 |
|
|
31
|
-
| diff-report | 版本间结构化差异 |
|
|
32
|
-
| preflight | 合并预演(冲突分级) |
|
|
33
|
-
| resolve-propose | AI 冲突分辨率提案 |
|
|
34
|
-
| merge-verified | 验证式合并(三道验证+落盘/拒绝) |
|
|
35
|
-
| rollback | 一键回滚到合并前快照 |
|
|
36
|
-
| ledger-query | 变更台账/审计查询 |
|
|
37
|
-
| ruleguard-scan | 规则衰减扫描 |
|
|
38
|
-
| ruleguard-compile | 规则编译 |
|
|
82
|
+
## Validator TestEvidence 桥
|
|
39
83
|
|
|
40
|
-
|
|
84
|
+
`merge-verified.validatorEvidence` 必须是统一 `cli.tax.test-evidence/1.0`:稳定 evidenceId、`kind: test`、`runner: trusted-runner`、command、exitCode、durationMs、summary、subject、subjectDigest 和 Validator execution receipt。subject 必须是包含冻结 GoldenBaseline 的完整 `validator.validation-subject/1.0`,不能用任意对象冒充 Validator 输出。
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
三层防护:规则编译 → 写入时拦截 → 跨分支规则一致性
|
|
86
|
+
MergeGuard 使用 `CLITAX_VALIDATOR_RECEIPT_PUBLIC_KEY` 验证 Ed25519 签名,并交叉验证 subject digest、runner、pass、exitCode、duration 和 summary。以下任一情况均 blocked:
|
|
44
87
|
|
|
45
|
-
|
|
88
|
+
- 缺少证据;
|
|
89
|
+
- `runner: local` 自报;
|
|
90
|
+
- receipt 缺失、签名错误、过期或跨 subject 重放;
|
|
91
|
+
- passed 非 true 或 exitCode 非 0。
|
|
46
92
|
|
|
47
|
-
|
|
48
|
-
合并候选(隔离区)
|
|
49
|
-
↓
|
|
50
|
-
① 结构效验:语法可解析、引用闭合
|
|
51
|
-
② 黄金基准:全量回归测试
|
|
52
|
-
③ 冒烟执行:沙箱跑通黄金路径
|
|
53
|
-
↓
|
|
54
|
-
全绿 → 落盘 + 更新台账
|
|
55
|
-
任一红 → 拒绝落盘 + AI 修复提案
|
|
56
|
-
```
|
|
93
|
+
证据可信只说明验证门通过,不代表仓库已合并。当前仍返回 `LOCAL-RUNNER-REQUIRED`;只有未来真实 runner 完成快照、落盘和复核后才能产生 merged 状态。
|
|
57
94
|
|
|
58
|
-
##
|
|
95
|
+
## 与技能链的合同
|
|
59
96
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
- Validator
|
|
97
|
+
- Aimlock:真实合并前必须先通过 mutate-gate,并把 scope/snapshot receipt 传给 local runner。
|
|
98
|
+
- Blueprint:结构合并仍为 planned;当前只能消费冲突上下文生成“不落盘”的 resolution proposal。
|
|
99
|
+
- Calctool:公式图节点合并为 planned,不得伪造对账结果。
|
|
100
|
+
- Swarm:可派发 local runner 任务,但 worker 自报不能成为 merge 证据。
|
|
101
|
+
- Validator:只接受签名 TestEvidence;MergeGuard 不复制或弱化 Validator 的终审规则。
|
|
65
102
|
|
|
66
103
|
## 反馈通道
|
|
67
104
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
评价只能由 IDE / 智能体自动回传,严禁人类干预:
|
|
106
|
+
|
|
107
|
+
1. 仅当一次真实技能 HTTP 调用成功,并且响应顶层包含 `feedbackInvocationId` 时,调用方才可进入评价流程。
|
|
108
|
+
2. 调用方必须使用本次响应的原始 ID,按真实执行结果生成整数分数(0–100)和真实评语,再自动执行:
|
|
109
|
+
`npx cli-mergeguard@latest feedback <feedbackInvocationId> <score> <comment...>`
|
|
110
|
+
3. 自动回传必须使用会员中心 Brain 客户端生成的同一身份文件,并通过 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 指向该 `0600` 文件;服务端以会员账号实名入库,不接受匿名评价。
|
|
111
|
+
4. 禁止伪造、猜测、复用或跨调用转移 `feedbackInvocationId`;禁止让人类选择分数、填写评价或代为执行命令。
|
|
112
|
+
5. 缺少会员客户端身份或 ID、分数越界、空评语、响应不合法及任何 HTTP 失败都必须视为回传失败,不得记为成功。
|
|
113
|
+
|
|
114
|
+
日常交流走技能详情页的独立聊天通道,不使用 `feedback` 命令,也不计入评价、评分或首页跑马灯。
|