新增 labels 下的韩泰 EOT 标注工作台

This commit is contained in:
Xin Wang
2026-10-09 13:12:52 +08:00
parent 1ab51c8bc1
commit 138b08a8e4
23 changed files with 4326 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
// @ts-nocheck -- Node's native TypeScript runner resolves explicit .ts imports.
import assert from "node:assert/strict";
import test from "node:test";
import {
createAnnotation,
exportAnnotations,
exportTranscripts,
formatTime,
getHistory,
parseAnnotations,
parseTranscripts,
reasonOptions,
validateAnnotation,
} from "../lib/annotations.ts";
const transcriptHeader = "call_id,role,start_ms,end_ms,transcript";
const annotationHeader = "call_id,decision_ms,user_text_at_decision,can_reply_now,confidence,reason,annotator";
const withoutId = (row) => Object.fromEntries(Object.entries(row).filter(([key]) => key !== "id"));
test("zero is a labeled negative example, and empty remains undecided", () => {
const rows = parseAnnotations(`\uFEFF${annotationHeader}\r\ndemo,5200,我明天下午,0,high,mid_turn_pause,A\r\ndemo,9400,听不清,,,audio_issue,A\r\n`);
assert.equal(rows[0].can_reply_now, "0");
assert.equal(rows[1].can_reply_now, "");
assert.equal(rows[1].confidence, "");
assert.notEqual(rows[0].id, rows[1].id);
assert.deepEqual(parseAnnotations(exportAnnotations(rows)).map(withoutId), rows.map(withoutId));
assert.equal(exportAnnotations(rows).split("\r\n")[0], annotationHeader);
assert.ok(!exportAnnotations(rows).includes(rows[0].id));
});
test("commas, doubled quotes and multiline UTF-8 text survive CSV round trips", () => {
const csv = `${transcriptHeader}\r\ndemo,user,100,800,"明天,\"\"下午\"\"\r\n都可以"\r\n`;
const rows = parseTranscripts(csv);
assert.equal(rows[0].transcript, '明天,"下午"\r\n都可以');
assert.deepEqual(parseTranscripts(exportTranscripts(rows)), rows);
const annotation = { ...createAnnotation("demo", 500), user_text_at_decision: '明天,"下午"\n先看看', annotator: 'A,"B"' };
assert.deepEqual(parseAnnotations(exportAnnotations([annotation])).map(withoutId), [withoutId(annotation)]);
});
test("required headers are validated while reordered columns remain usable", () => {
assert.equal(parseTranscripts("transcript,end_ms,role,call_id,start_ms\n可以,40,user,demo,20")[0].transcript, "可以");
assert.throws(() => parseTranscripts("call_id,role,start_ms,end_ms\ndemo,user,0,10"), /缺少字段.*transcript/);
assert.throws(() => parseTranscripts(`${transcriptHeader},extra\ndemo,user,0,10,可以,x`), /未知字段.*extra/);
assert.throws(() => parseTranscripts("call_id,role,start_ms,end_ms,transcript,role"), /重复字段/);
assert.throws(() => parseAnnotations(""), /为空/);
assert.deepEqual(parseAnnotations(annotationHeader), []);
});
test("malformed CSV and incorrect column counts give useful line diagnostics", () => {
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,"未闭合`), /第 2 行.*没有闭合/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,"可以"额外`), /结束引号后/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,可以"`), /双引号.*转义/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,多,一列`), /第 2 行.*实际有 6 列/);
});
test("invalid times and roles are rejected instead of being silently coerced", () => {
for (const start of ["", "-1", "1.5", "1e3", "9007199254740992"]) {
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,${start},2000,可以`), /非负整数毫秒/);
}
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,assistant,0,100,可以`), /agent 或 user/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,100,100,可以`), /小于 end_ms/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\n,user,0,100,可以`), /call_id 不能为空/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,-1,可以,1,high,completed_answer,A`), /非负整数毫秒/);
});
test("labels, confidence, known reasons and reason-label consistency are checked", () => {
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,2,high,completed_answer,A`), /can_reply_now 只能/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,strong,completed_answer,A`), /confidence 只能/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,high,freeform,A`), /未知的 reason/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,0,high,completed_answer,A`), /不一致/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,high,uncertain,A`), /不一致/);
for (const option of reasonOptions) {
const row = { ...createAnnotation("demo", 100), can_reply_now: option.can_reply_now, reason: option.value };
assert.equal(validateAnnotation(row), null);
}
});
test("duplicate decision positions are rejected within a call and allowed across calls", () => {
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,high,completed_answer,A\ndemo,100,等等,0,high,hesitation,B`), /已有标注/);
const first = createAnnotation("demo", 100);
const duplicate = createAnnotation("demo", 100);
assert.match(validateAnnotation(duplicate, [first]), /已有标注/);
assert.equal(validateAnnotation(first, [first]), null);
assert.throws(() => exportAnnotations([first, first]), /重复/);
assert.equal(parseAnnotations(`${annotationHeader}\ndemo,100,,,,,\nother,100,,,,,`).length, 2);
});
test("history uses all earlier completed records, excludes crossing and future records, and preserves tied order", () => {
const rows = parseTranscripts(`${transcriptHeader}\ndemo,user,300,440,我明天下午\ndemo,agent,100,240,什么时候方便\ndemo,user,400,700,不行后天可以\nother,user,0,5,其他通话\ndemo,user,300,400,同时出现的第二行\ndemo,agent,800,900,后天上午可以吗`);
const before = rows.slice();
assert.deepEqual(getHistory(rows, "demo", 520).map((row) => row.transcript), ["什么时候方便", "我明天下午", "同时出现的第二行"]);
assert.deepEqual(getHistory(rows, "demo", 440).map((row) => row.transcript), ["什么时候方便", "我明天下午", "同时出现的第二行"]);
assert.deepEqual(getHistory(rows, "demo", 100), []);
assert.deepEqual(rows, before);
});
test("new samples start undecided and time formatting preserves millisecond precision", () => {
const row = createAnnotation("demo", 0);
assert.equal(row.can_reply_now, "");
assert.equal(row.reason, "");
assert.equal(validateAnnotation(row), null);
assert.equal(formatTime(0), "00:00.000");
assert.equal(formatTime(5_200), "00:05.200");
assert.equal(formatTime(62_345), "01:02.345");
assert.equal(formatTime(3_600_000), "60:00.000");
assert.throws(() => createAnnotation("demo", -1), /非负整数/);
});
@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import test from "node:test";
// @ts-expect-error Node's native TypeScript runner resolves explicit .ts imports.
import { isSameOriginRequest } from "../lib/request-origin.ts";
function request(headers: Record<string, string>, url = "http://localhost:3000/api/workspace"): Request {
return new Request(url, { method: "POST", headers });
}
test("127.0.0.1 page may save when Next normalizes request.url to localhost", () => {
assert.equal(isSameOriginRequest(request({
Host: "127.0.0.1:3000",
Origin: "http://127.0.0.1:3000",
"Sec-Fetch-Site": "same-origin",
})), true);
});
test("localhost and IPv6 same-origin hosts may save", () => {
for (const host of ["localhost:3000", "[::1]:3000"]) {
assert.equal(isSameOriginRequest(request({ Host: host, Origin: `http://${host}` })), true);
}
});
test("evil origin and cross-site fetches remain forbidden", () => {
assert.equal(isSameOriginRequest(request({ Host: "127.0.0.1:3000", Origin: "https://evil.example" })), false);
assert.equal(isSameOriginRequest(request({
Host: "127.0.0.1:3000", Origin: "http://127.0.0.1:3000", "Sec-Fetch-Site": "cross-site",
})), false);
});
test("forwarded-host cannot override the actual Host header", () => {
assert.equal(isSameOriginRequest(request({
Host: "localhost:3000", Origin: "https://evil.example", "X-Forwarded-Host": "evil.example",
})), false);
});
test("port/protocol mismatches and malformed origins are forbidden", () => {
for (const origin of ["http://localhost:3001", "https://localhost:3000", "null", "broken", "http://localhost:3000/path"]) {
assert.equal(isSameOriginRequest(request({ Host: "localhost:3000", Origin: origin })), false);
}
});
test("non-browser clients without Origin remain supported, and URL is a Host fallback", () => {
assert.equal(isSameOriginRequest(request({})), true);
assert.equal(isSameOriginRequest(request({ Origin: "http://localhost:3000" })), true);
assert.equal(isSameOriginRequest(request({ "Sec-Fetch-Site": "cross-site" })), false);
});