dsh-theone 0.3.20 → 0.3.22
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/README.md +109 -14
- package/README.zh.md +110 -15
- package/dist/catalog-types.d.ts +5 -0
- package/dist/client-locales.d.ts +39 -0
- package/dist/client-locales.js +78 -0
- package/dist/client.js +423 -43
- package/dist/fact-flow.d.ts +39 -0
- package/dist/fact-flow.js +135 -0
- package/dist/facts.d.ts +127 -0
- package/dist/facts.js +177 -0
- package/dist/feedback.d.ts +113 -0
- package/dist/feedback.js +146 -0
- package/dist/history-catalog.d.ts +9 -0
- package/dist/history-catalog.js +48 -2
- package/dist/index.d.ts +37 -1
- package/dist/index.js +318 -28
- package/dist/linkage.d.ts +9 -0
- package/dist/linkage.js +31 -1
- package/dist/llm-router.d.ts +8 -1
- package/dist/llm-router.js +29 -6
- package/dist/run.d.ts +2 -0
- package/dist/run.js +2 -0
- package/dist/settings-types.d.ts +6 -2
- package/dist/settings-types.js +3 -3
- package/dist/settings.js +3 -2
- package/dist/store.d.ts +104 -2
- package/dist/store.js +269 -2
- package/dist/topic-memory.d.ts +61 -0
- package/dist/topic-memory.js +78 -0
- package/dist/types.d.ts +4 -0
- package/package.json +22 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ContextStore } from './store.ts';
|
|
2
|
+
import { type LinkScope } from './linkage.ts';
|
|
3
|
+
import { type EvidenceEvent, type FactCandidate, type FactView } from './facts.ts';
|
|
4
|
+
/** Confirmed facts this message might use, best first, within budget. Ranking only. */
|
|
5
|
+
export declare function factCandidates(store: ContextStore, scope: LinkScope, text: string, recent: string[], currentId?: string): FactCandidate[];
|
|
6
|
+
export interface FactDelivery {
|
|
7
|
+
notices: string[];
|
|
8
|
+
facts: string[];
|
|
9
|
+
/** Record what was sent; call only once the briefing carrying these lines is actually sent. */
|
|
10
|
+
commit(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* What a topic is told about facts before its Worker answers: changes to facts it used before
|
|
14
|
+
* (also on a plain "go on") and the facts this request imports. Every fact is checked here, against
|
|
15
|
+
* the topic finally chosen and the settings now; nothing is recorded until `commit`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function factDelivery(store: ContextStore, scope: LinkScope, contextId: string, imports: string[], inputId?: string): FactDelivery;
|
|
18
|
+
/** The topic's own recorded facts, with ids and versions, for its Worker's descriptor. */
|
|
19
|
+
export declare function ownFactsText(store: ContextStore, contextId: string): string;
|
|
20
|
+
/** Optional extraction after a turn; every item it returns is checked like a Worker's own record. */
|
|
21
|
+
export declare const EXTRACT_PROMPT = "\u4F60\u5728\u5E2E\u4E00\u4E2A\u8BDD\u9898\u8BB0\u5F55\u5DF2\u7ECF\u786E\u5B9A\u4E0B\u6765\u7684\u8981\u70B9\uFF1A\u6570\u5B57\u3001\u65E5\u671F\u3001\u51B3\u5B9A\u3001\u6587\u4EF6\u6216\u94FE\u63A5\u7684\u4F4D\u7F6E\u3002\u8F93\u5165\u662F\u672C\u8F6E\u7684\u7528\u6237\u6D88\u606F user\u3001\u52A9\u624B\u56DE\u7B54 assistant\uFF0C\u4EE5\u53CA\u672C\u8BDD\u9898\u5DF2\u8BB0\u5F55\u7684\u8981\u70B9 facts\uFF08\u542B factId \u548C\u7248\u672C\uFF09\u3002\n\u53EA\u8BB0\u5F55\u7528\u6237\u4EB2\u53E3\u8BF4\u51FA\u7684\u503C\uFF0C\u6216\u7528\u6237\u660E\u786E\u63A5\u53D7\u4E86\u52A9\u624B\u63D0\u8BAE\u7684\u503C\uFF08\u6B64\u65F6 acceptsQuote \u586B\u52A9\u624B\u63D0\u8BAE\u7684\u539F\u8BDD\uFF09\uFF1B\u52A9\u624B\u81EA\u5DF1\u7684\u5EFA\u8BAE\u3001\u63A8\u6D4B\u3001\u672A\u5B9A\u7684\u4E8B\u4E0D\u8981\u8BB0\u5F55\u3002\n\u6BCF\u6761\u90FD\u5FC5\u987B\u7ED9\u51FA evidenceQuote\uFF1A\u672C\u8F6E\u5BF9\u8BDD\u91CC\u80FD\u8BC1\u660E\u5B83\u7684\u539F\u8BDD\uFF0C\u9010\u5B57\u590D\u5236\u3002\u5DF2\u8BB0\u5F55\u8FC7\u7684\u540C\u4E00\u4EF6\u4E8B\u7528\u5B83\u7684 factId \u66F4\u65B0\uFF1B\u7528\u6237\u8BF4\u67D0\u4E2A\u503C\u4E0D\u518D\u6210\u7ACB\u6216\u8FD8\u6CA1\u5B9A\u65F6\uFF0C\u8F93\u51FA op \u4E3A \"retract\" \u5E76\u7ED9\u51FA\u7528\u6237\u539F\u8BDD\u3002\u6CA1\u6709\u5C31\u8F93\u51FA\u7A7A\u6570\u7EC4\u3002\n\u53EA\u8F93\u51FA JSON \u6570\u7EC4\uFF0C\u4F8B\u5982 [{\"op\":\"set\",\"factId\":null,\"label\":\"\u9884\u7B97\",\"kind\":\"fact\",\"value\":\"800\",\"evidenceQuote\":\"\u9884\u7B97\u5B9A\u4E3A 800 \u5143\"}]\u3002kind \u53EA\u80FD\u662F fact\u3001decision\u3001artifact\u3002";
|
|
22
|
+
/** The extraction request for the turn that just ended: cleaned, and within its size budget. */
|
|
23
|
+
export declare function extractionPayload(events: readonly EvidenceEvent[], base: readonly FactView[]): {
|
|
24
|
+
user: string;
|
|
25
|
+
assistant: string;
|
|
26
|
+
facts: object[];
|
|
27
|
+
} | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Store what an extraction returned, by the same rules as a Worker's record: evidence is checked in
|
|
30
|
+
* the session, and every write expects the version the topic had when the turn ended, so a late
|
|
31
|
+
* extraction cannot overwrite a newer value. Returns how many items were stored.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyExtraction(store: ContextStore, input: {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
contextId: string;
|
|
36
|
+
events: readonly EvidenceEvent[];
|
|
37
|
+
base: readonly FactView[];
|
|
38
|
+
items: unknown;
|
|
39
|
+
}): number;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { LINK_SIGNAL, mayShare, relatedTopics } from "./linkage.js";
|
|
2
|
+
import { FACT_KINDS, FACT_LIMITS, factKey, factLine, findQuote, rankCandidates, retracts, safe, verify, within } from "./facts.js";
|
|
3
|
+
/** Confirmed facts this message might use, best first, within budget. Ranking only. */
|
|
4
|
+
export function factCandidates(store, scope, text, recent, currentId) {
|
|
5
|
+
if (scope === 'off')
|
|
6
|
+
return [];
|
|
7
|
+
// A topic whose conversations are gone or archived offers nothing (see HistoryCatalog.reconcile).
|
|
8
|
+
const hidden = store.hiddenReasons();
|
|
9
|
+
const titles = new Map(store.contexts().filter(context => !hidden.has(context.id)).map(context => [context.id, context.title]));
|
|
10
|
+
const related = new Map(currentId ? relatedTopics(store, currentId, scope, 10).map(topic => [topic.id, topic.score]) : []);
|
|
11
|
+
// The current topic's own facts are candidates too: the message may be moving elsewhere with them.
|
|
12
|
+
// A first filter for the topic in view; delivery checks again against the topic finally chosen.
|
|
13
|
+
const pool = store.sharedFacts().filter(fact => titles.has(fact.contextId) &&
|
|
14
|
+
(currentId ? !mayShare(store, scope, fact.contextId, currentId) : !store.isPrivate(fact.contextId)))
|
|
15
|
+
.map(fact => ({ ...fact, topicTitle: titles.get(fact.contextId), related: related.get(fact.contextId) ?? 0 }));
|
|
16
|
+
return rankCandidates(pool, [...recent.slice(-2), text].join('\n'));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What a topic is told about facts before its Worker answers: changes to facts it used before
|
|
20
|
+
* (also on a plain "go on") and the facts this request imports. Every fact is checked here, against
|
|
21
|
+
* the topic finally chosen and the settings now; nothing is recorded until `commit`.
|
|
22
|
+
*/
|
|
23
|
+
export function factDelivery(store, scope, contextId, imports, inputId) {
|
|
24
|
+
const hidden = store.hiddenReasons();
|
|
25
|
+
const titles = new Map(store.contexts().filter(context => !hidden.has(context.id)).map(context => [context.id, context.title]));
|
|
26
|
+
const allowed = (sourceId) => titles.has(sourceId) && !mayShare(store, scope, sourceId, contextId);
|
|
27
|
+
const notices = [];
|
|
28
|
+
const quiet = [];
|
|
29
|
+
const noticed = new Set();
|
|
30
|
+
for (const dependency of store.dependencies(contextId)) {
|
|
31
|
+
const fact = store.fact(dependency.factId);
|
|
32
|
+
const label = safe(fact?.label ?? '一个要点', 40);
|
|
33
|
+
if (!fact || fact.deletedAt !== undefined || !allowed(fact.contextId)) {
|
|
34
|
+
// Only the name: a topic that may no longer share must not leak its new value.
|
|
35
|
+
notices.push({ line: `- 之前引用的「${label}」不再可用,不要再使用它之前的值。`, commit: () => store.forgetDependency(contextId, dependency.factId) });
|
|
36
|
+
}
|
|
37
|
+
else if (fact.version > dependency.versionSeen) {
|
|
38
|
+
const title = safe(titles.get(fact.contextId), 40);
|
|
39
|
+
const before = store.factVersion(fact.id, dependency.versionSeen);
|
|
40
|
+
// The same value said again (a reaffirmation is a new version): nothing to tell; move the mark on.
|
|
41
|
+
if (before && before.status === fact.status && before.value === fact.value) {
|
|
42
|
+
quiet.push(() => store.recordDelivery(contextId, fact, 'notice', inputId));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const line = fact.status !== 'confirmed' ? `- 【${title}】${label}:已撤回或目前没有确认的值,不要再使用之前的值。`
|
|
46
|
+
: before?.status === 'confirmed' ? `- 【${title}】${label}:${safe(before.value ?? '', 80)} → ${safe(fact.value ?? '', 80)}(第 ${fact.version} 版)`
|
|
47
|
+
: `- 【${title}】${label}:重新确认为 ${safe(fact.value ?? '', 80)}(第 ${fact.version} 版)`;
|
|
48
|
+
notices.push({ line, commit: () => store.recordDelivery(contextId, fact, 'notice', inputId) });
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
continue;
|
|
52
|
+
noticed.add(dependency.factId);
|
|
53
|
+
}
|
|
54
|
+
const facts = [];
|
|
55
|
+
for (const id of new Set(imports)) {
|
|
56
|
+
const fact = store.fact(id);
|
|
57
|
+
if (!fact || noticed.has(id) || fact.deletedAt !== undefined || fact.status !== 'confirmed' || fact.contextId === contextId || !allowed(fact.contextId))
|
|
58
|
+
continue;
|
|
59
|
+
facts.push({ line: factLine(fact, titles.get(fact.contextId)), commit: () => {
|
|
60
|
+
store.recordDelivery(contextId, fact, 'route', inputId);
|
|
61
|
+
store.learnLink(contextId, fact.contextId, LINK_SIGNAL.mention);
|
|
62
|
+
} });
|
|
63
|
+
}
|
|
64
|
+
const fit = (items, budget) => items.slice(0, within(items.map(item => item.line), budget).length);
|
|
65
|
+
const sentNotices = fit(notices, FACT_LIMITS.noticeBudget), sentFacts = fit(facts, FACT_LIMITS.briefingBudget);
|
|
66
|
+
return { notices: sentNotices.map(item => item.line), facts: sentFacts.map(item => item.line),
|
|
67
|
+
commit: () => { for (const item of [...sentNotices, ...sentFacts])
|
|
68
|
+
item.commit(); for (const mark of quiet)
|
|
69
|
+
mark(); } };
|
|
70
|
+
}
|
|
71
|
+
/** The topic's own recorded facts, with ids and versions, for its Worker's descriptor. */
|
|
72
|
+
export function ownFactsText(store, contextId) {
|
|
73
|
+
const lines = store.facts(contextId, true).slice(0, FACT_LIMITS.ownItems).map(fact => `- [${fact.id} v${fact.version}] ${safe(fact.label, FACT_LIMITS.label)} = ${safe(fact.value ?? '', 120)}${fact.status === 'proposed' ? '(建议,用户未确认)' : fact.status === 'retracted' ? '(已撤回)' : ''}`);
|
|
74
|
+
const kept = within(lines, FACT_LIMITS.ownBudget);
|
|
75
|
+
return kept.length ? `\n本话题已记录的要点(更新或撤回时用 theone_record,带上 factId 和 expectedVersion):\n${kept.join('\n')}` : '';
|
|
76
|
+
}
|
|
77
|
+
/** Optional extraction after a turn; every item it returns is checked like a Worker's own record. */
|
|
78
|
+
export const EXTRACT_PROMPT = `你在帮一个话题记录已经确定下来的要点:数字、日期、决定、文件或链接的位置。输入是本轮的用户消息 user、助手回答 assistant,以及本话题已记录的要点 facts(含 factId 和版本)。
|
|
79
|
+
只记录用户亲口说出的值,或用户明确接受了助手提议的值(此时 acceptsQuote 填助手提议的原话);助手自己的建议、推测、未定的事不要记录。
|
|
80
|
+
每条都必须给出 evidenceQuote:本轮对话里能证明它的原话,逐字复制。已记录过的同一件事用它的 factId 更新;用户说某个值不再成立或还没定时,输出 op 为 "retract" 并给出用户原话。没有就输出空数组。
|
|
81
|
+
只输出 JSON 数组,例如 [{"op":"set","factId":null,"label":"预算","kind":"fact","value":"800","evidenceQuote":"预算定为 800 元"}]。kind 只能是 fact、decision、artifact。`;
|
|
82
|
+
/** The extraction request for the turn that just ended: cleaned, and within its size budget. */
|
|
83
|
+
export function extractionPayload(events, base) {
|
|
84
|
+
const lastUser = events.findLastIndex(event => event.speaker === 'user');
|
|
85
|
+
if (lastUser < 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
// An accepted proposal is quoted from the reply before the user's words, so that reply is included.
|
|
88
|
+
const previous = events.slice(0, lastUser).findLast(event => event.speaker === 'assistant');
|
|
89
|
+
const user = events[lastUser].text, assistant = [previous?.text, ...events.slice(lastUser + 1).filter(event => event.speaker === 'assistant').map(event => event.text)].filter(Boolean).join('\n');
|
|
90
|
+
const known = within(base.map(fact => JSON.stringify({ factId: fact.id, version: fact.version, status: fact.status, label: safe(fact.label, FACT_LIMITS.label), value: safe(fact.value ?? '', 120) })), FACT_LIMITS.extractionFacts);
|
|
91
|
+
const room = FACT_LIMITS.extractionBudget - known.join('').length;
|
|
92
|
+
const payload = { user: safe(user, Math.floor(room / 3)), assistant: safe(assistant, room - Math.floor(room / 3) - 200), facts: known.map(line => JSON.parse(line)) };
|
|
93
|
+
// Escaping quotes, newlines and backslashes can double the serialized request size.
|
|
94
|
+
while (JSON.stringify(payload).length > FACT_LIMITS.extractionBudget && (payload.user || payload.assistant)) {
|
|
95
|
+
const field = payload.assistant.length >= payload.user.length ? 'assistant' : 'user';
|
|
96
|
+
const excess = JSON.stringify(payload).length - FACT_LIMITS.extractionBudget;
|
|
97
|
+
payload[field] = payload[field].slice(0, Math.max(0, payload[field].length - Math.ceil(excess / 2)));
|
|
98
|
+
}
|
|
99
|
+
return payload;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Store what an extraction returned, by the same rules as a Worker's record: evidence is checked in
|
|
103
|
+
* the session, and every write expects the version the topic had when the turn ended, so a late
|
|
104
|
+
* extraction cannot overwrite a newer value. Returns how many items were stored.
|
|
105
|
+
*/
|
|
106
|
+
export function applyExtraction(store, input) {
|
|
107
|
+
if (!Array.isArray(input.items))
|
|
108
|
+
return 0;
|
|
109
|
+
const byId = new Map(input.base.map(fact => [fact.id, fact.version]));
|
|
110
|
+
const byName = new Map(input.base.flatMap(fact => [fact.key, ...fact.aliases].map(key => [key, fact.version])));
|
|
111
|
+
let stored = 0;
|
|
112
|
+
for (const item of input.items.slice(0, FACT_LIMITS.recordItems)) {
|
|
113
|
+
if (!item || typeof item !== 'object')
|
|
114
|
+
continue;
|
|
115
|
+
const text = (key) => typeof item[key] === 'string' ? item[key] : undefined;
|
|
116
|
+
const quote = text('evidenceQuote') ?? '', factId = text('factId'), label = text('label') ?? input.base.find(fact => fact.id === factId)?.label ?? '';
|
|
117
|
+
// A name that did not exist when the turn ended must still not exist: expected version 0.
|
|
118
|
+
const expectedVersion = factId ? byId.get(factId) : byName.get(factKey(label)) ?? 0;
|
|
119
|
+
if (factId && expectedVersion === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
if (item.op === 'retract') {
|
|
122
|
+
const said = factId ? findQuote(input.events, quote, { speaker: 'user' }) : undefined;
|
|
123
|
+
if (said && retracts(quote) && retracts(said.text) && store.retractFact(input.contextId, factId, { sessionId: input.sessionId, seq: said.seq, speaker: 'user', quote }, 'extractor', expectedVersion).outcome === 'retracted')
|
|
124
|
+
stored++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const kind = (FACT_KINDS.includes(item.kind) ? item.kind : 'fact');
|
|
128
|
+
const verdict = verify({ sessionId: input.sessionId, events: input.events, kind, value: text('value') ?? '', quote, acceptsQuote: text('acceptsQuote') });
|
|
129
|
+
const result = store.recordFact(input.contextId, { ...(factId ? { factId } : {}), label, kind, value: text('value') ?? '', status: verdict.status,
|
|
130
|
+
evidence: verdict.evidence, origin: 'extractor', expectedVersion });
|
|
131
|
+
if (result.outcome === 'created' || result.outcome === 'updated')
|
|
132
|
+
stored++;
|
|
133
|
+
}
|
|
134
|
+
return stored;
|
|
135
|
+
}
|
package/dist/facts.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
2
|
+
export type FactKind = 'fact' | 'decision' | 'artifact';
|
|
3
|
+
export type FactStatus = 'confirmed' | 'proposed' | 'retracted';
|
|
4
|
+
export type Speaker = 'user' | 'assistant' | 'tool';
|
|
5
|
+
export declare const FACT_KINDS: readonly FactKind[];
|
|
6
|
+
/** Where a version came from. `seq` is null when the quoted words were not found in the session. */
|
|
7
|
+
export interface FactEvidence {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
seq: number | null;
|
|
10
|
+
speaker: Speaker | 'unverified';
|
|
11
|
+
quote: string;
|
|
12
|
+
/** For a confirmation such as "ok, go with that": the assistant's proposal it accepted. */
|
|
13
|
+
accepts?: {
|
|
14
|
+
seq: number;
|
|
15
|
+
quote: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** A fact as it stands now: its identity and its current version. */
|
|
19
|
+
export interface FactView {
|
|
20
|
+
id: string;
|
|
21
|
+
contextId: string;
|
|
22
|
+
key: string;
|
|
23
|
+
label: string;
|
|
24
|
+
aliases: string[];
|
|
25
|
+
kind: FactKind;
|
|
26
|
+
version: number;
|
|
27
|
+
status: FactStatus;
|
|
28
|
+
value: string | null;
|
|
29
|
+
evidence: FactEvidence;
|
|
30
|
+
origin: 'worker' | 'extractor';
|
|
31
|
+
createdAt: number;
|
|
32
|
+
deletedAt?: number;
|
|
33
|
+
mergedFrom?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Every size limit of the feature in one place; each is a hard cap on characters or items. */
|
|
36
|
+
export declare const FACT_LIMITS: {
|
|
37
|
+
readonly label: 60;
|
|
38
|
+
readonly value: 300;
|
|
39
|
+
readonly quote: 200;
|
|
40
|
+
readonly aliases: 6;
|
|
41
|
+
readonly alias: 60;
|
|
42
|
+
readonly recordItems: 8;
|
|
43
|
+
readonly routerBudget: 1200;
|
|
44
|
+
readonly routerLabel: 40;
|
|
45
|
+
readonly routerValue: 80;
|
|
46
|
+
readonly routerItems: 12;
|
|
47
|
+
readonly imports: 5;
|
|
48
|
+
readonly briefingBudget: 1200;
|
|
49
|
+
readonly noticeBudget: 600;
|
|
50
|
+
readonly ownBudget: 1500;
|
|
51
|
+
readonly ownItems: 20;
|
|
52
|
+
readonly lookupItems: 10;
|
|
53
|
+
readonly extractionBudget: 6000;
|
|
54
|
+
readonly extractionFacts: 1500;
|
|
55
|
+
};
|
|
56
|
+
/** Text that may be stored or shown: credentials and personal addresses removed, then shortened. */
|
|
57
|
+
export declare const safe: (text: string, max: number) => string;
|
|
58
|
+
/**
|
|
59
|
+
* The name two writes must share to be the same fact. Case, width and spacing are folded, and only
|
|
60
|
+
* wrapping quotes and punctuation are dropped, so `C++`, `C#` and `.NET` stay distinct.
|
|
61
|
+
*/
|
|
62
|
+
export declare function factKey(label: string): string;
|
|
63
|
+
/** Folded text for finding a quote inside a message: width, case, spacing and thousands separators. */
|
|
64
|
+
export declare function matchText(text: string): string;
|
|
65
|
+
/** Literal evidence, with complete numeric tokens and units; similarity is for ranking, not proof. */
|
|
66
|
+
export declare function states(source: string, value: string): boolean;
|
|
67
|
+
/** "ok, go with that": an explicit acceptance, without a refusal and not itself a question. */
|
|
68
|
+
export declare function accepts(text: string): boolean;
|
|
69
|
+
/** A retraction needs words withdrawing or reopening a value, not merely any user quote. */
|
|
70
|
+
export declare function retracts(text: string): boolean;
|
|
71
|
+
export interface EvidenceEvent {
|
|
72
|
+
seq: number;
|
|
73
|
+
speaker: Speaker;
|
|
74
|
+
text: string;
|
|
75
|
+
toolResult?: boolean;
|
|
76
|
+
isError?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The messages of a topic session that can serve as evidence, newest last: the user's own messages,
|
|
80
|
+
* the assistant's answers and tool activity. TheOne's reference material for the session (its
|
|
81
|
+
* descriptor, cross-topic briefings) is not the user speaking and never counts.
|
|
82
|
+
*/
|
|
83
|
+
export declare function evidenceEvents(events: readonly SessionEvent[], limit?: number): EvidenceEvent[];
|
|
84
|
+
/** The newest event containing `quote` (folded), optionally only from one speaker or before a point. */
|
|
85
|
+
export declare function findQuote(events: readonly EvidenceEvent[], quote: string, options?: {
|
|
86
|
+
speaker?: Speaker;
|
|
87
|
+
before?: number;
|
|
88
|
+
}): EvidenceEvent | undefined;
|
|
89
|
+
export interface Verdict {
|
|
90
|
+
status: 'confirmed' | 'proposed';
|
|
91
|
+
evidence: FactEvidence;
|
|
92
|
+
reason?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Decide from the session itself whether a value was confirmed. The model only points at words; the
|
|
96
|
+
* speaker and the position come from the session. Confirmed means: the user stated the value; or the
|
|
97
|
+
* user explicitly accepted an earlier assistant proposal that stated it; or, for an artifact, a tool
|
|
98
|
+
* produced it. Anything else is a proposal and stays inside its topic.
|
|
99
|
+
*/
|
|
100
|
+
export declare function verify(input: {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
events: readonly EvidenceEvent[];
|
|
103
|
+
kind: FactKind;
|
|
104
|
+
value: string;
|
|
105
|
+
quote: string;
|
|
106
|
+
acceptsQuote?: string;
|
|
107
|
+
}): Verdict;
|
|
108
|
+
/** One fact as other topics are shown it: `【topic】label = value (version n, confirmed at …)`. */
|
|
109
|
+
export declare function factLine(fact: FactView, topicTitle: string): string;
|
|
110
|
+
/** Lines up to a character budget, whole lines only. */
|
|
111
|
+
export declare function within(lines: string[], budget: number): string[];
|
|
112
|
+
export interface FactCandidate {
|
|
113
|
+
id: string;
|
|
114
|
+
topic: string;
|
|
115
|
+
label: string;
|
|
116
|
+
kind: FactKind;
|
|
117
|
+
value: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Facts from other topics that the message may draw on, best first. Ranking only: whether a fact may
|
|
121
|
+
* actually be delivered is decided again at delivery, against the final topic and current settings.
|
|
122
|
+
*/
|
|
123
|
+
export declare function rankCandidates(facts: readonly (FactView & {
|
|
124
|
+
topicTitle: string;
|
|
125
|
+
related: number;
|
|
126
|
+
lastUsedAt?: number;
|
|
127
|
+
})[], text: string, now?: number): FactCandidate[];
|
package/dist/facts.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { redactRoutingText, textFeatures } from "./routing-policy.js";
|
|
2
|
+
export const FACT_KINDS = ['fact', 'decision', 'artifact'];
|
|
3
|
+
/** Every size limit of the feature in one place; each is a hard cap on characters or items. */
|
|
4
|
+
export const FACT_LIMITS = {
|
|
5
|
+
label: 60, value: 300, quote: 200, aliases: 6, alias: 60, recordItems: 8,
|
|
6
|
+
routerBudget: 1200, routerLabel: 40, routerValue: 80, routerItems: 12, imports: 5,
|
|
7
|
+
briefingBudget: 1200, noticeBudget: 600, ownBudget: 1500, ownItems: 20, lookupItems: 10,
|
|
8
|
+
extractionBudget: 6000, extractionFacts: 1500,
|
|
9
|
+
};
|
|
10
|
+
const clip = (text, max) => text.length > max ? text.slice(0, Math.max(0, max - 1)) + '…' : text;
|
|
11
|
+
/** Text that may be stored or shown: credentials and personal addresses removed, then shortened. */
|
|
12
|
+
export const safe = (text, max) => clip(redactRoutingText(text).trim(), max);
|
|
13
|
+
/**
|
|
14
|
+
* The name two writes must share to be the same fact. Case, width and spacing are folded, and only
|
|
15
|
+
* wrapping quotes and punctuation are dropped, so `C++`, `C#` and `.NET` stay distinct.
|
|
16
|
+
*/
|
|
17
|
+
export function factKey(label) {
|
|
18
|
+
return label.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim()
|
|
19
|
+
.replace(/^[\s"'“”‘’「」『』《》〈〉()()[\]【】,;:!?,。;:!?、]+/, '')
|
|
20
|
+
.replace(/[\s"'“”‘’「」『』《》〈〉()()[\]【】.,;:!?,。;:!?、]+$/, '');
|
|
21
|
+
}
|
|
22
|
+
/** Folded text for finding a quote inside a message: width, case, spacing and thousands separators. */
|
|
23
|
+
export function matchText(text) {
|
|
24
|
+
return redactRoutingText(text).normalize('NFKC').toLowerCase().replace(/(\d)[,,](?=\d{3}\b)/g, '$1').replace(/\s+/g, '');
|
|
25
|
+
}
|
|
26
|
+
/** Literal evidence, with complete numeric tokens and units; similarity is for ranking, not proof. */
|
|
27
|
+
export function states(source, value) {
|
|
28
|
+
const s = matchText(source), v = matchText(value);
|
|
29
|
+
if (!v || !s)
|
|
30
|
+
return false;
|
|
31
|
+
const present = new Set(numberTokens(source));
|
|
32
|
+
if (numberTokens(value).some(number => !present.has(number)))
|
|
33
|
+
return false;
|
|
34
|
+
return s.includes(v);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whole numbers as written, read before spaces are folded away (so "12 3pm" is 12 and 3, not 123).
|
|
38
|
+
* A sign only where no digit precedes it, so "2026-10-05" holds 10 and 05, not -10 and -05.
|
|
39
|
+
*/
|
|
40
|
+
function numberTokens(text) {
|
|
41
|
+
return redactRoutingText(text).normalize('NFKC').replace(/(\d)[,,](?=\d{3}\b)/g, '$1').match(/(?<![\d.])[+-]?\d+(?:\.\d+)?/g) ?? [];
|
|
42
|
+
}
|
|
43
|
+
const NEGATION = /(不行|不可以|不好|不要|不用|不能|不是|不再|不同意|不采用|不选|别这样|算了|再想想|\bno\b|\bnot\b|\bdon'?t\b|\bnope\b|rather than|instead of)/i;
|
|
44
|
+
const UNCERTAIN = /(未定|没定|没(?:有)?确定|未确认|未确定|不确定|暂定|待定|建议|假设|如果|或许|可能|\b(might|maybe|perhaps|suggest|suppose|if|undecided)\b)/i;
|
|
45
|
+
const QUESTION = /[??]|(吗|呢)\s*[。!!]?\s*$/;
|
|
46
|
+
const ACCEPTANCE = /((?:^|[\s,,。.!!;;])(?:好的?|行|可以|确定|定了)(?=$|[\s,,。.!!;;]|就|按)|就按|就这样|就用|就它|同意|就这么定|没问题|按你说的|\bok(ay)?\b|\byes\b|\bsure\b|sounds good|go with|let'?s (do|go|use)|\bagreed?\b|\bdeal\b|works for me|that works)/i;
|
|
47
|
+
/** Naming the assistant's suggestion ("就按你的建议", "your suggestion") is how an acceptance points at it. */
|
|
48
|
+
const THEIR_SUGGESTION = /((?:你|您)(?:的|刚才的|说的)?(?:建议|提议|方案))|\byour (?:suggestion|proposal|idea)\b/gi;
|
|
49
|
+
/** "ok, go with that": an explicit acceptance, without a refusal and not itself a question. */
|
|
50
|
+
export function accepts(text) {
|
|
51
|
+
return ACCEPTANCE.test(text) && !NEGATION.test(text) && !UNCERTAIN.test(text.replace(THEIR_SUGGESTION, '')) && !QUESTION.test(text.trim());
|
|
52
|
+
}
|
|
53
|
+
/** A retraction needs words withdrawing or reopening a value, not merely any user quote. */
|
|
54
|
+
export function retracts(text) {
|
|
55
|
+
return !QUESTION.test(text.trim()) && /(撤回|取消|不再|不要|不用|未定|没定|没(?:有)?确定|未确认|未确定|不确定|先不定|待定|\b(withdraw|withdrawn|cancel|cancelled|canceled|undecided)\b|no longer|is off|not (yet )?(settled|decided))/i.test(text);
|
|
56
|
+
}
|
|
57
|
+
/** Check the actual sentence, including words a shortened model quote might have omitted. */
|
|
58
|
+
function assertsValue(text, value, quote) {
|
|
59
|
+
const needle = matchText(quote);
|
|
60
|
+
const clauses = text.replace(/(\d)[,,](?=\d{3}\b)/g, '$1').split(/(?<=[。!?!?;;\n])/).flatMap(sentence => sentence.split(/[,,]/).filter(clause => states(clause, value) &&
|
|
61
|
+
(matchText(clause).includes(needle) || needle.includes(matchText(clause))))
|
|
62
|
+
.map(clause => ({ text: clause, question: QUESTION.test(sentence.trim()) })));
|
|
63
|
+
return clauses.length > 0 && clauses.every(clause => !clause.question && !NEGATION.test(clause.text) && !UNCERTAIN.test(clause.text));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The messages of a topic session that can serve as evidence, newest last: the user's own messages,
|
|
67
|
+
* the assistant's answers and tool activity. TheOne's reference material for the session (its
|
|
68
|
+
* descriptor, cross-topic briefings) is not the user speaking and never counts.
|
|
69
|
+
*/
|
|
70
|
+
export function evidenceEvents(events, limit = 200) {
|
|
71
|
+
const text = (blocks) => blocks.flatMap(block => block.type === 'text' && block.text ? [block.text] : []).join('\n');
|
|
72
|
+
// TheOne's own tools repeat the quotes they are given; a call citing itself proves nothing.
|
|
73
|
+
const own = new Set(events.flatMap(event => event.type === 'tool/call' && event.data.name.startsWith('theone_') ? [String(event.data.callId)] : []));
|
|
74
|
+
const result = [];
|
|
75
|
+
for (const event of events) {
|
|
76
|
+
if (event.type === 'user/message' && event.data.source.kind === 'user')
|
|
77
|
+
result.push({ seq: event.seq, speaker: 'user', text: text(event.data.content) });
|
|
78
|
+
else if (event.type === 'assistant/message')
|
|
79
|
+
result.push({ seq: event.seq, speaker: 'assistant', text: text(event.data.message.content) });
|
|
80
|
+
else if (event.type === 'tool/result' && !own.has(String(event.data.message.source.callId)))
|
|
81
|
+
result.push({ seq: event.seq, speaker: 'tool', text: text(event.data.message.content), toolResult: true, isError: event.data.message.isError === true });
|
|
82
|
+
}
|
|
83
|
+
return result.filter(item => item.text.trim()).slice(-limit);
|
|
84
|
+
}
|
|
85
|
+
/** The newest event containing `quote` (folded), optionally only from one speaker or before a point. */
|
|
86
|
+
export function findQuote(events, quote, options = {}) {
|
|
87
|
+
const needle = matchText(quote);
|
|
88
|
+
if (needle.length < 2)
|
|
89
|
+
return undefined;
|
|
90
|
+
for (let index = events.length - 1; index >= 0; index--) {
|
|
91
|
+
const event = events[index];
|
|
92
|
+
if (options.before !== undefined && event.seq >= options.before)
|
|
93
|
+
continue;
|
|
94
|
+
if (options.speaker && event.speaker !== options.speaker)
|
|
95
|
+
continue;
|
|
96
|
+
if (matchText(event.text).includes(needle))
|
|
97
|
+
return event;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Decide from the session itself whether a value was confirmed. The model only points at words; the
|
|
103
|
+
* speaker and the position come from the session. Confirmed means: the user stated the value; or the
|
|
104
|
+
* user explicitly accepted an earlier assistant proposal that stated it; or, for an artifact, a tool
|
|
105
|
+
* produced it. Anything else is a proposal and stays inside its topic.
|
|
106
|
+
*/
|
|
107
|
+
export function verify(input) {
|
|
108
|
+
const quote = safe(input.quote, FACT_LIMITS.quote);
|
|
109
|
+
// The user's own message wins over an echo of it (the assistant repeating what the user said).
|
|
110
|
+
const hit = findQuote(input.events, input.quote, { speaker: 'user' }) ?? findQuote(input.events, input.quote);
|
|
111
|
+
if (!hit)
|
|
112
|
+
return { status: 'proposed', reason: 'quote-not-found', evidence: { sessionId: input.sessionId, seq: null, speaker: 'unverified', quote } };
|
|
113
|
+
const evidence = { sessionId: input.sessionId, seq: hit.seq, speaker: hit.speaker, quote };
|
|
114
|
+
if (hit.speaker === 'user' && states(input.quote, input.value) && assertsValue(hit.text, input.value, input.quote))
|
|
115
|
+
return { status: 'confirmed', evidence };
|
|
116
|
+
if (hit.speaker === 'user' && input.acceptsQuote && accepts(input.quote) && accepts(hit.text)) {
|
|
117
|
+
const proposal = findQuote(input.events, input.acceptsQuote, { speaker: 'assistant', before: hit.seq });
|
|
118
|
+
const latest = input.events.findLast(event => event.speaker === 'assistant' && event.seq < hit.seq);
|
|
119
|
+
if (proposal && proposal.seq === latest?.seq && states(input.acceptsQuote, input.value))
|
|
120
|
+
return { status: 'confirmed', evidence: { ...evidence, accepts: { seq: proposal.seq, quote: safe(input.acceptsQuote, FACT_LIMITS.quote) } } };
|
|
121
|
+
return { status: 'proposed', reason: 'proposal-not-found', evidence };
|
|
122
|
+
}
|
|
123
|
+
if (input.kind === 'artifact' && hit.speaker === 'tool' && hit.toolResult && !hit.isError && states(hit.text, input.value))
|
|
124
|
+
return { status: 'confirmed', evidence };
|
|
125
|
+
return { status: 'proposed', reason: hit.speaker === 'user' ? 'value-not-in-quote' : `said-by-${hit.speaker}`, evidence };
|
|
126
|
+
}
|
|
127
|
+
/** One fact as other topics are shown it: `【topic】label = value (version n, confirmed at …)`. */
|
|
128
|
+
export function factLine(fact, topicTitle) {
|
|
129
|
+
const at = new Date(fact.createdAt).toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
|
|
130
|
+
const confirmedBy = fact.evidence.speaker === 'tool' ? '工具验证于' : '用户确认于';
|
|
131
|
+
return `- 【${safe(topicTitle, 40)}】${safe(fact.label, FACT_LIMITS.label)} = ${safe(fact.value ?? '', FACT_LIMITS.value)}(第 ${fact.version} 版,${confirmedBy} ${at})`;
|
|
132
|
+
}
|
|
133
|
+
/** Lines up to a character budget, whole lines only. */
|
|
134
|
+
export function within(lines, budget) {
|
|
135
|
+
const kept = [];
|
|
136
|
+
let used = 0;
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (used + line.length + 1 > budget)
|
|
139
|
+
break;
|
|
140
|
+
kept.push(line);
|
|
141
|
+
used += line.length + 1;
|
|
142
|
+
}
|
|
143
|
+
return kept;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Facts from other topics that the message may draw on, best first. Ranking only: whether a fact may
|
|
147
|
+
* actually be delivered is decided again at delivery, against the final topic and current settings.
|
|
148
|
+
*/
|
|
149
|
+
export function rankCandidates(facts, text, now = Date.now()) {
|
|
150
|
+
const query = textFeatures(text);
|
|
151
|
+
const lower = text.toLowerCase();
|
|
152
|
+
const scored = facts.flatMap(fact => {
|
|
153
|
+
const words = textFeatures([fact.label, ...fact.aliases, fact.value ?? ''].join(' '));
|
|
154
|
+
let shared = 0;
|
|
155
|
+
for (const feature of words)
|
|
156
|
+
if (query.has(feature))
|
|
157
|
+
shared++;
|
|
158
|
+
const lexical = words.size ? shared / Math.sqrt(words.size * Math.max(1, query.size)) : 0;
|
|
159
|
+
const named = fact.topicTitle && lower.includes(fact.topicTitle.toLowerCase()) ? 0.3 : 0;
|
|
160
|
+
const fresh = 0.1 * 0.5 ** ((now - (fact.lastUsedAt ?? fact.createdAt)) / (14 * 86400000));
|
|
161
|
+
const score = lexical + named + Math.min(0.3, fact.related * 0.1) + fresh;
|
|
162
|
+
return lexical > 0 || named > 0 ? [{ fact, score }] : [];
|
|
163
|
+
}).sort((a, b) => b.score - a.score);
|
|
164
|
+
const result = [];
|
|
165
|
+
let used = 2; // The serialized array's brackets and commas count towards the router budget too.
|
|
166
|
+
for (const { fact } of scored) {
|
|
167
|
+
if (result.length >= FACT_LIMITS.routerItems)
|
|
168
|
+
break;
|
|
169
|
+
const item = { id: fact.id, topic: clip(fact.topicTitle, 40), label: clip(fact.label, FACT_LIMITS.routerLabel), kind: fact.kind, value: safe(fact.value ?? '', FACT_LIMITS.routerValue) };
|
|
170
|
+
const size = JSON.stringify(item).length + (result.length ? 1 : 0);
|
|
171
|
+
if (used + size > FACT_LIMITS.routerBudget)
|
|
172
|
+
break;
|
|
173
|
+
result.push(item);
|
|
174
|
+
used += size;
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import type { RouteView } from './types.ts';
|
|
3
|
+
/** Where reports go: a small Cloudflare Worker run by TheOne's maintainer (server/feedback). */
|
|
4
|
+
export declare const FEEDBACK_URL = "https://feedback.yulid.org/v1/reports";
|
|
5
|
+
export declare const FEEDBACK_EMAIL = "theone@yulid.org";
|
|
6
|
+
export declare const FEEDBACK_LIMITS: {
|
|
7
|
+
readonly description: 4000;
|
|
8
|
+
readonly contact: 200;
|
|
9
|
+
readonly routes: 30;
|
|
10
|
+
readonly replyText: 8000;
|
|
11
|
+
readonly body: 60000;
|
|
12
|
+
};
|
|
13
|
+
/** Strings as they may leave this computer: secrets, emails, addresses and the home folder removed. */
|
|
14
|
+
export declare function scrub(value: unknown, home?: string): unknown;
|
|
15
|
+
/** Recent routes without their text or topics: what happened, how, how long, and any error code. */
|
|
16
|
+
export declare function routeDigest(routes: readonly RouteView[], now?: number): {
|
|
17
|
+
corrected?: boolean | undefined;
|
|
18
|
+
error?: string | undefined;
|
|
19
|
+
ms?: number | undefined;
|
|
20
|
+
model?: string | undefined;
|
|
21
|
+
via?: "rules" | "llm" | undefined;
|
|
22
|
+
minutesAgo: number;
|
|
23
|
+
action: import("./types.ts").Action;
|
|
24
|
+
reason: string;
|
|
25
|
+
status: "planned" | "running" | "completed" | "failed";
|
|
26
|
+
}[];
|
|
27
|
+
/**
|
|
28
|
+
* One exchange in a session: the user message `messageId` (or, failing that, the latest user message
|
|
29
|
+
* with the same text) and every assistant text up to the next user message.
|
|
30
|
+
*/
|
|
31
|
+
export declare function exchange(events: readonly SessionEvent[], messageId: string, sameText?: string): {
|
|
32
|
+
user: string;
|
|
33
|
+
reply: string;
|
|
34
|
+
steps: number;
|
|
35
|
+
} | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Whether main chat showed what the topic's own session said, and signs of garbled text in either:
|
|
38
|
+
* replacement characters (U+FFFD), stray control characters, and long runs of one repeated character.
|
|
39
|
+
*/
|
|
40
|
+
export declare function compareReplies(main: string, worker: string): {
|
|
41
|
+
main: {
|
|
42
|
+
chars: number;
|
|
43
|
+
replacement: number;
|
|
44
|
+
control: number;
|
|
45
|
+
repeatedRuns: number;
|
|
46
|
+
};
|
|
47
|
+
worker: {
|
|
48
|
+
chars: number;
|
|
49
|
+
replacement: number;
|
|
50
|
+
control: number;
|
|
51
|
+
repeatedRuns: number;
|
|
52
|
+
};
|
|
53
|
+
firstDifference?: number | undefined;
|
|
54
|
+
identical: boolean;
|
|
55
|
+
};
|
|
56
|
+
/** The part of a report about one message: always the comparison, the texts only when the user agreed. */
|
|
57
|
+
export declare function replySection(route: RouteView, main: ReturnType<typeof exchange>, worker: ReturnType<typeof exchange>, includeText: boolean, now?: number): {
|
|
58
|
+
topicSessionReply?: string | undefined;
|
|
59
|
+
mainChatReply?: string | undefined;
|
|
60
|
+
message?: string | undefined;
|
|
61
|
+
comparison?: {
|
|
62
|
+
main: {
|
|
63
|
+
chars: number;
|
|
64
|
+
replacement: number;
|
|
65
|
+
control: number;
|
|
66
|
+
repeatedRuns: number;
|
|
67
|
+
};
|
|
68
|
+
worker: {
|
|
69
|
+
chars: number;
|
|
70
|
+
replacement: number;
|
|
71
|
+
control: number;
|
|
72
|
+
repeatedRuns: number;
|
|
73
|
+
};
|
|
74
|
+
firstDifference?: number | undefined;
|
|
75
|
+
identical: boolean;
|
|
76
|
+
} | undefined;
|
|
77
|
+
topicSteps?: number | undefined;
|
|
78
|
+
mainSteps?: number | undefined;
|
|
79
|
+
route: {
|
|
80
|
+
corrected?: boolean | undefined;
|
|
81
|
+
error?: string | undefined;
|
|
82
|
+
ms?: number | undefined;
|
|
83
|
+
model?: string | undefined;
|
|
84
|
+
via?: "rules" | "llm" | undefined;
|
|
85
|
+
minutesAgo: number;
|
|
86
|
+
action: import("./types.ts").Action;
|
|
87
|
+
reason: string;
|
|
88
|
+
status: "planned" | "running" | "completed" | "failed";
|
|
89
|
+
};
|
|
90
|
+
found: {
|
|
91
|
+
mainChat: boolean;
|
|
92
|
+
topicSession: boolean;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
export interface FeedbackDraft {
|
|
96
|
+
v: 1;
|
|
97
|
+
app: 'theone';
|
|
98
|
+
version: string;
|
|
99
|
+
diagnostics: Record<string, unknown>;
|
|
100
|
+
reply?: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
export interface FeedbackReport extends FeedbackDraft {
|
|
103
|
+
lang: 'zh' | 'en';
|
|
104
|
+
description: string;
|
|
105
|
+
contact?: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The report to send: the draft the user saw plus what they typed. The draft comes back from the
|
|
109
|
+
* page, so it is checked and scrubbed again here; anything malformed is refused, not repaired.
|
|
110
|
+
*/
|
|
111
|
+
export declare function finalReport(input: Record<string, unknown>, version: string, home?: string): FeedbackReport;
|
|
112
|
+
/** Send a report; resolves to its id, or throws RATE_LIMITED, REJECTED, UNREACHABLE or SERVER_ERROR. */
|
|
113
|
+
export declare function sendReport(report: FeedbackReport, url?: string, fetcher?: typeof fetch): Promise<string>;
|