dsh-task-dispatch-table 0.0.0 → 0.1.1
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 +101 -31
- package/README.zh.md +119 -0
- package/assets/icon-scheduler.svg +1 -0
- package/cordis.patch.yml +9 -0
- package/dist/attachment-allowlist.d.ts +24 -0
- package/dist/attachment-allowlist.js +41 -0
- package/dist/calendar-plan.d.ts +55 -0
- package/dist/calendar-plan.js +104 -0
- package/dist/client.js +72655 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +80 -0
- package/dist/deps.d.ts +25 -0
- package/dist/deps.js +54 -0
- package/dist/dispatch.d.ts +133 -0
- package/dist/dispatch.js +509 -0
- package/dist/event-bus.d.ts +16 -0
- package/dist/event-bus.js +67 -0
- package/dist/event-catalog.d.ts +42 -0
- package/dist/event-catalog.js +60 -0
- package/dist/host.d.ts +299 -0
- package/dist/host.js +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +0 -0
- package/dist/receipt.d.ts +38 -0
- package/dist/receipt.js +236 -0
- package/dist/reconcile.d.ts +105 -0
- package/dist/reconcile.js +641 -0
- package/dist/runtime-index.d.ts +102 -0
- package/dist/runtime-index.js +340 -0
- package/dist/schedule-next.d.ts +44 -0
- package/dist/schedule-next.js +216 -0
- package/dist/scheduler.d.ts +82 -0
- package/dist/scheduler.js +639 -0
- package/dist/store.d.ts +461 -0
- package/dist/store.js +845 -0
- package/dist/submit.d.ts +1 -0
- package/dist/submit.js +60 -0
- package/dist/task-assets.d.ts +124 -0
- package/dist/task-assets.js +380 -0
- package/dist/task-sort.d.ts +27 -0
- package/dist/task-sort.js +59 -0
- package/dist/tasks.d.ts +191 -0
- package/dist/tasks.js +527 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +67 -6
- package/index.js +0 -7
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具名 = **包名去掉 `dsh-` 前缀 + `_receipt`** ⇒ `task_dispatch_table_receipt`。
|
|
3
|
+
*
|
|
4
|
+
* 为什么带命名空间而不是短名:宿主对重名的处理是「**同层抛错 / 跨层 scoped 遮蔽 global**」
|
|
5
|
+
* (`@deepseek-ai/dsh-tools`:`NamedEntries` 冲突直接 throw;类注释 `Scoped registrations
|
|
6
|
+
* shadow globals`)——两种都不希望发生。带插件名前缀把撞名概率压到可忽略,也让模型一眼看出归属。
|
|
7
|
+
* 宿主对工具名没有格式/长度校验,所以这纯粹是防撞考虑。
|
|
8
|
+
*/
|
|
9
|
+
export const RECEIPT_TOOL_NAME = 'task_dispatch_table_receipt';
|
|
10
|
+
/** 可提交回执的实例状态(与 submit.ts 的判定一致)。 */
|
|
11
|
+
const RECEIPT_ACCEPTING = ['dispatched', 'running', 'unknown'];
|
|
12
|
+
const asRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: {};
|
|
15
|
+
/** 文本归一:去零宽字符(模型偶发插入 U+200B..U+200D / U+FEFF)+ 去首尾空白。 */
|
|
16
|
+
const normalizeText = (value) => value.replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
|
17
|
+
/**
|
|
18
|
+
* 全角 → 半角折叠(含全角空格 U+3000):中文语境下模型偶发输出 `ok` / `ok `。
|
|
19
|
+
* ⚠️ 只用于 `status` 比对,不碰 `note` 正文(中文标点是正常内容,不该被改写)。
|
|
20
|
+
*/
|
|
21
|
+
const foldFullWidth = (value) => value
|
|
22
|
+
.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0))
|
|
23
|
+
.replace(/\u3000/g, ' ');
|
|
24
|
+
/**
|
|
25
|
+
* status 归一(**宽松接受、存声明值**):大小写不敏感 + 去空白/零宽字符后与
|
|
26
|
+
* `contract.validStatuses` 比对,命中则**存回任务声明的那个值**。
|
|
27
|
+
*
|
|
28
|
+
* 这样做的两个理由:① 对账侧的 `validStatuses.includes(status)` 永远成立(因为存的就是声明值);
|
|
29
|
+
* ② agent 写 `OK` / `ok ` / `ok` 之类不会被判非法而白白重试一轮(无人值守链路里,一次重试
|
|
30
|
+
* 就是一轮 token)。其余写法(如 `success`、`完成`)一律不认,如实性不受影响。
|
|
31
|
+
*
|
|
32
|
+
* @returns 任务声明的规范值;没命中返回 undefined(调用方按「参数不合法」告知并让其修正)。
|
|
33
|
+
*/
|
|
34
|
+
function normalizeStatus(raw, statuses) {
|
|
35
|
+
if (typeof raw !== 'string')
|
|
36
|
+
return undefined;
|
|
37
|
+
const wanted = foldFullWidth(normalizeText(raw)).toLowerCase();
|
|
38
|
+
return statuses.find(status => status.toLowerCase() === wanted);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* outputs 归一:数组取字符串项;字符串按逗号切(模型偶尔传逗号串);其余忽略。
|
|
42
|
+
* 逐项去空白 / 零宽字符、反斜杠统一成 `/`、去掉 `./` 前缀(宿主对账按工作区相对路径 `resolve`)。
|
|
43
|
+
*/
|
|
44
|
+
function normalizeOutputs(raw) {
|
|
45
|
+
const list = typeof raw === 'string' ? raw.split(',') : Array.isArray(raw) ? raw : [];
|
|
46
|
+
return list
|
|
47
|
+
.filter((item) => typeof item === 'string')
|
|
48
|
+
.map(item => normalizeText(item).replace(/\\/g, '/').replace(/^\.\//, ''))
|
|
49
|
+
.filter(item => item.length > 0);
|
|
50
|
+
}
|
|
51
|
+
/** 合法 status 清单(空数组兜底成 `['ok']`:schema 允许显式传空,但工具的 enum 不能为空)。 */
|
|
52
|
+
function receiptStatuses(validStatuses) {
|
|
53
|
+
return validStatuses.length > 0 ? validStatuses : ['ok'];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 工具定义(**裸 definition**、零依赖):宿主 `register` 只强校验
|
|
57
|
+
* `output: { schema, render, presentationMeta? }`,参数 schema 是纯 JSON Schema,
|
|
58
|
+
* 故无需引宿主包(`defineTool` 那套要引 `@deepseek-ai/dsh-tools`,本插件刻意不依赖宿主运行时包)。
|
|
59
|
+
*/
|
|
60
|
+
function buildDefinition(deps) {
|
|
61
|
+
const { store, taskName, instanceId, sessionId, logger, sessionProjections } = deps;
|
|
62
|
+
const statuses = receiptStatuses(deps.validStatuses);
|
|
63
|
+
return {
|
|
64
|
+
name: RECEIPT_TOOL_NAME,
|
|
65
|
+
// 工具描述是**常驻上下文里的另一个指令面**(用户 2026-09-30:与消息体是两条独立通道,两边都要压硬)。
|
|
66
|
+
// 时机由「任务做完后」改成「**本轮回复结束前**」——真机失败模式正是"回了一句就当收工";
|
|
67
|
+
// outputs 的填写粒度放这里(属"怎么填",不再占用消息体那段的篇幅)。
|
|
68
|
+
description: `提交任务「${taskName}」的执行回执。**本轮回复结束前必须调用一次**(准备输出最后一句之前);`
|
|
69
|
+
+ `调度器以它判定本次任务成败,不调用等于失败——任务指令说「不要做其他操作」也不豁免。`
|
|
70
|
+
+ `参数:status(必填,如实)、outputs(可选,本次真实交付物相对工作区根的路径)、note(可选备注)。`
|
|
71
|
+
+ `outputs 粒度:装产物的文件夹是为本任务专门建的(如 "web-app/")→ 填文件夹路径;`
|
|
72
|
+
+ `只是写进既有或按规范建的目录(如按日期的日常目录)→ 逐个列文件路径(如 ["20260928/a.md"]);`
|
|
73
|
+
+ `**以最后一次提交为准**:可以重复调用,但每一次都会整体覆盖上一次——所以再次提交时,`
|
|
74
|
+
+ `之前已经报过的产物必须**一并带上**(不回带 = 视为放弃,它们会丢)。没有产出就省略 outputs。`,
|
|
75
|
+
parameters: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
properties: {
|
|
79
|
+
status: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
enum: [...statuses],
|
|
82
|
+
description: `执行结果,必须如实,只能取:${statuses.join(' / ')}`,
|
|
83
|
+
},
|
|
84
|
+
outputs: {
|
|
85
|
+
type: 'array',
|
|
86
|
+
items: { type: 'string' },
|
|
87
|
+
description: '本次真实交付物,相对工作区根。粒度判断:装产物的文件夹是为本任务专门建的(如网页/项目专属文件夹)→ 填文件夹路径(如 "web-app/");文件只是写进既有或按规范建的目录(如按日期的日常目录)→ 逐个列文件路径。可同时含多个目录与多个文件;没有产出可省略。',
|
|
88
|
+
},
|
|
89
|
+
note: {
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: '可选备注,例如失败原因或关键结论。',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ['status'],
|
|
95
|
+
},
|
|
96
|
+
output: {
|
|
97
|
+
schema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
properties: {
|
|
101
|
+
ok: { type: 'boolean' },
|
|
102
|
+
message: { type: 'string' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
render: (_args, value) => [
|
|
106
|
+
{ type: 'text', text: value.message },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
execute: async (args, exec) => {
|
|
110
|
+
try {
|
|
111
|
+
const input = asRecord(args);
|
|
112
|
+
const rawStatus = typeof input.status === 'string' ? input.status : '';
|
|
113
|
+
const status = normalizeStatus(input.status, statuses);
|
|
114
|
+
if (status === undefined) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
message: `回执未提交:status "${rawStatus}" 不在允许值内(只能是 ${statuses.join(' | ')};`
|
|
118
|
+
+ `大小写与首尾空白会自动归一,其余写法一律不认)。请修正参数后重试一次;这是参数错误,不必等待。`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const outputs = normalizeOutputs(input.outputs);
|
|
122
|
+
const rawNote = typeof input.note === 'string' ? normalizeText(input.note) : '';
|
|
123
|
+
const note = rawNote.length > 0 ? rawNote : undefined;
|
|
124
|
+
const instance = store.get(instanceId);
|
|
125
|
+
if (instance === undefined) {
|
|
126
|
+
return { ok: false, message: `回执未提交:实例 ${instanceId} 不存在。请停止重试并说明情况。` };
|
|
127
|
+
}
|
|
128
|
+
if (!RECEIPT_ACCEPTING.includes(instance.status)) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
message: `回执未提交:实例 ${instanceId} 已是终态(${instance.status}),回执不再计入。不必重试。`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (instance.session_id !== sessionId) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
message: `回执未提交:实例当前会话已是 ${instance.session_id ?? 'null'},本会话无权提交。不必重试。`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 与 submit.ts 写进 receipt 事件的形状完全一致(对账逻辑零改动):status/outputs/note/session_id。
|
|
141
|
+
store.appendEvent(instanceId, 'receipt', { status, outputs, note, session_id: sessionId });
|
|
142
|
+
logger.info(`回执已记录 ${instanceId}: status=${status}${outputs.length > 0 ? `, outputs=${outputs.length} 项` : ''}`);
|
|
143
|
+
// B 路线:插件代写官方交付事件(决策 40 演进:插件作唯一写入方,禁止 LLM 调 present)。
|
|
144
|
+
// 失败只记日志、绝不拖垮回执(外层 try 已兜底,这里再独立 try 防任何意外上抛)。
|
|
145
|
+
try {
|
|
146
|
+
const execCtx = exec;
|
|
147
|
+
const session = execCtx?.agent?.session;
|
|
148
|
+
const callId = execCtx?.callId;
|
|
149
|
+
const projections = sessionProjections;
|
|
150
|
+
if (session !== undefined && callId !== undefined && projections !== undefined) {
|
|
151
|
+
const boundary = projections.stateOf(session, 'turnBoundary');
|
|
152
|
+
const turn = boundary?.lastTurn;
|
|
153
|
+
if (typeof turn === 'number' && turn >= 1) {
|
|
154
|
+
;
|
|
155
|
+
session.append('deliverables/presented', {
|
|
156
|
+
turn,
|
|
157
|
+
callId,
|
|
158
|
+
files: outputs.map((path) => ({ path })),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
logger.warn(`交付事件跳过(turnBoundary.lastTurn 缺失)${instanceId}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
logger.warn(`交付事件跳过(session/callId/sessionProjections 缺失)${instanceId}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (deliverErr) {
|
|
170
|
+
logger.warn(`交付事件写入失败 ${instanceId}: ${String(deliverErr)}`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
message: `回执已记录(${instanceId},status=${status}`
|
|
175
|
+
+ `${outputs.length > 0 ? `,outputs ${outputs.length} 项` : ''})。任务结束,无需再做任何事。`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
// 写库异常(连接已关、磁盘满等):明确告知可重试次数与停止条件(决策 24 提示词策略)。
|
|
180
|
+
logger.warn(`回执写入失败 ${instanceId}: ${String(error)}`);
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
message: `回执未提交:写入状态库失败(${String(error)})。`
|
|
184
|
+
+ `请等约 10 秒后原样重试,最多 3 次;仍失败就立即停止,不要尝试其他任何手段。`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 把回执工具注册进该 agent 的作用域(**只在 setup 里调**——发布前生效,见文件头)。
|
|
192
|
+
* @returns true = 已注册;false = 宿主 agent 作用域未暴露 tools 服务(跳过并告警:
|
|
193
|
+
* 该会话没有回执通道,对账会按「缺回执」收敛,reason 会指向这里)。
|
|
194
|
+
*/
|
|
195
|
+
export function registerReceiptTool(agentCtx, deps) {
|
|
196
|
+
const tools = agentCtx?.tools;
|
|
197
|
+
if (tools === undefined || typeof tools.register !== 'function') {
|
|
198
|
+
deps.logger.warn(`agent 作用域未暴露 tools 服务,${RECEIPT_TOOL_NAME} 未注册——本会话无法提交回执(决策 24)`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
tools.register(buildDefinition(deps));
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
deps.logger.warn(`注册工具 ${RECEIPT_TOOL_NAME} 失败: ${String(error)}`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 回执调用说明(派发消息与追问消息共用)。写给模型看:**传什么、失败怎么办、什么时候必须停**。
|
|
212
|
+
*
|
|
213
|
+
* ⚠️ 刻意不提供任何替代通道(不跑命令、不写库、不碰沙箱)——真机上 agent 曾自行 `chmod`、
|
|
214
|
+
* 拷库、改用 sqlite3/node 绕道,全是无效动作(沙箱层面就不可能成功),白烧 token。
|
|
215
|
+
*/
|
|
216
|
+
export function receiptInstruction(validStatuses) {
|
|
217
|
+
const statuses = receiptStatuses(validStatuses);
|
|
218
|
+
// ⚠️ 写法原则(用户 2026-09-30 拍板 + 真机证据,改前 10 行、改后 7 行):
|
|
219
|
+
// ① 只讲这一件事,**越短越硬**(长段会被模型当背景噪音忽略);
|
|
220
|
+
// ② 把"压过任务指令"写成**显式优先级裁决句** —— 模型对"最高优先级"这种形容词不敏感,对"谁跟谁冲突、
|
|
221
|
+
// 以谁为准"的裁决句敏感;
|
|
222
|
+
// ③ 时机写成「**本轮回复结束前**」——真机失败正是"回了一句就当收工";
|
|
223
|
+
// ④ 本段**只在派发消息最末出现一处**(同一条指令出现两处,模型会挑最弱的一处遵守)。
|
|
224
|
+
// 具体的 outputs 填写粒度挪进**工具描述**(模型看得到、且常驻上下文),本段不重复。
|
|
225
|
+
return [
|
|
226
|
+
`【回执·唯一权威段】本轮回复结束前,必须调用一次 ${RECEIPT_TOOL_NAME}。`,
|
|
227
|
+
`⚖️ 冲突裁决:任务提示词里任何「不要做其他操作 / 只做某件事 / 什么都不用做 / 只回复一句话」的说法,`
|
|
228
|
+
+ `对回执**不生效**——它约束的是任务内容,不约束回执。不提交 = 本次任务失败。`,
|
|
229
|
+
`- 没有产出文件也要交(省略 outputs)。`,
|
|
230
|
+
`- **以最后一次提交为准**:重复提交会整体覆盖,先前报过的产物必须一并带上(不回带 = 放弃)。`,
|
|
231
|
+
`${RECEIPT_TOOL_NAME}({ status: "${statuses[0] ?? 'ok'}", outputs: ["<产物,相对工作区根路径>"] })`,
|
|
232
|
+
`- status 只能取:${statuses.join(' | ')}(如实填写)。`,
|
|
233
|
+
`- 调用失败:等 10 秒**原样重试**,最多 3 次;仍失败就**停下**(不要跑命令、不要动状态库、不要绕过沙箱)。`,
|
|
234
|
+
`- 不要调用 present:交付卡片由插件按 outputs 统一生成。`,
|
|
235
|
+
].join('\n');
|
|
236
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { HostContext, HostLogger, HostSession } from './host.js';
|
|
2
|
+
import type { TaskDefinition } from './tasks.js';
|
|
3
|
+
import type { AgentHandle } from './dispatch.js';
|
|
4
|
+
import type { InstanceSnapshot, TaskInstance, TaskStore } from './store.js';
|
|
5
|
+
import type { RuntimeIndex } from './runtime-index.js';
|
|
6
|
+
import { type PushEvent } from './event-catalog.js';
|
|
7
|
+
import type { PluginConfig } from './config.js';
|
|
8
|
+
import { type AssetPaths } from './task-assets.js';
|
|
9
|
+
/**
|
|
10
|
+
* 快照里的附加文件是否还在(Loop B 发动前兜底):返回**缺失**的展示名。
|
|
11
|
+
* upload 型按任务目录绝对路径(需 task_id),link 型按快照里冻结的工作区路径。
|
|
12
|
+
* 拿不到基准 ⇒ 跳过不判,绝不猜。
|
|
13
|
+
*/
|
|
14
|
+
export declare function missingSnapshotAttachments(ctx: HostContext, taskId: string, snap: InstanceSnapshot, assets: AssetPaths | null): string[];
|
|
15
|
+
export interface ReconcileOptions {
|
|
16
|
+
leaseMs: number;
|
|
17
|
+
dispatchGraceMs: number;
|
|
18
|
+
unknownGraceMs: number;
|
|
19
|
+
/** 插件配置(决策 41:模型漏斗②③④层在发动时现算用;属插件级参数,非任务设置)。 */
|
|
20
|
+
config(): PluginConfig;
|
|
21
|
+
/**
|
|
22
|
+
* 旧实例无快照时的一次性兼容(决策 41):按任务定义当场合成快照并固化进实例行;
|
|
23
|
+
* 返回 undefined = 无法兜底(任务已删 / 工作区没了)⇒ 该实例如实按失败收敛。
|
|
24
|
+
*/
|
|
25
|
+
legacyTask?(taskId: string): TaskDefinition | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* 任务文件资产根(2026-09-30):发动前再次校验附加文件还在不在(Loop A 已查过一次,
|
|
28
|
+
* 这里是落库后到发动之间的兜底)。未就绪 ⇒ 跳过 upload 型校验,不误拦。
|
|
29
|
+
*/
|
|
30
|
+
assets?(): AssetPaths | null;
|
|
31
|
+
}
|
|
32
|
+
export interface Reconciler {
|
|
33
|
+
/** 发动成功后登记 agent handle(决策 19:超时追问用),终态/重试时自动遗忘。 */
|
|
34
|
+
registerHandle(sessionId: string, handle: AgentHandle): void;
|
|
35
|
+
onCreated(session: HostSession): void;
|
|
36
|
+
onEvent(session: HostSession, event: {
|
|
37
|
+
type: string;
|
|
38
|
+
}): void;
|
|
39
|
+
onDisposed(session: HostSession): void;
|
|
40
|
+
/** 发动异常等场景的重试判定入口(§6)。 */
|
|
41
|
+
retryOrFail(instance: TaskInstance, reason: string, detail?: unknown): void;
|
|
42
|
+
sweep(): void;
|
|
43
|
+
}
|
|
44
|
+
export interface ReconcilerDeps {
|
|
45
|
+
ctx: HostContext;
|
|
46
|
+
/** tee logger(显式传参——ctx 不可包装,见 host.ts HostLogger 注释)。 */
|
|
47
|
+
logger: HostLogger;
|
|
48
|
+
store: TaskStore;
|
|
49
|
+
options: ReconcileOptions;
|
|
50
|
+
/** 主界面运行态内存索引(2026-09-30);未装配则跳过(不影响对账)。 */
|
|
51
|
+
runtime?: RuntimeIndex;
|
|
52
|
+
/** 事件广播(design/event-push.md):实例行在 DB 层的变化也通知前端;未装配则跳过。 */
|
|
53
|
+
emit?: (event: PushEvent) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 回执裁决(决策 19,替代旧契约文件三查):
|
|
57
|
+
* receipt 事件存在 + status ∈ validStatuses + outputs 里的每个路径**确实存在**。
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ **已去掉「mtime 新鲜度」闸**(用户 2026-10-03 拍板,原为「防旧产物冒充」):
|
|
60
|
+
* 那道闸会误伤「复用 / 检查已有文件」类任务 —— 真机案例:任务是判断 `uuid.txt`
|
|
61
|
+
* 是否存在(存在就不动它),agent 如实回执 `outputs:["uuid.txt"]`,但文件本来就在、
|
|
62
|
+
* 没被改写 ⇒ mtime 早于派发时刻 ⇒ 被判 `output-stale` 失败,用户看到的却是「文件明明在」。
|
|
63
|
+
*
|
|
64
|
+
* 用户口径(原话):「**只要他交出来的文件确实存在、格式是对的,就不用管**」;
|
|
65
|
+
* 「大模型是不是企图蒙混过关,你不用去管」——**任务做得怎么样是大模型的事,
|
|
66
|
+
* 插件只确认它确实执行了**。故只保留「存在性」一道闸,不再替模型判断产出新鲜度。
|
|
67
|
+
*
|
|
68
|
+
* status 仍必须如实 ∈ validStatuses(agent 自报不可信,决策 11)。
|
|
69
|
+
* 决策 41:workspacePath / validStatuses 来自派发快照,与任务设置无关。
|
|
70
|
+
*/
|
|
71
|
+
export declare function checkReceipt(workspacePath: string, validStatuses: readonly string[], receipt: {
|
|
72
|
+
ts: string;
|
|
73
|
+
detail: string | null;
|
|
74
|
+
} | undefined): {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
reason?: string;
|
|
77
|
+
detail?: unknown;
|
|
78
|
+
};
|
|
79
|
+
/** token 用量分量(决策 32 修订:不再记单一总数,按输入/输出/缓存拆分)。 */
|
|
80
|
+
export interface TokenUsage {
|
|
81
|
+
/** 输入(prompt)token。 */
|
|
82
|
+
in?: number;
|
|
83
|
+
/** 输出(completion)token。 */
|
|
84
|
+
out?: number;
|
|
85
|
+
/** 命中上下文缓存的输入 token。 */
|
|
86
|
+
cache?: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 从会话事件里取 token 用量分量(决策 32 修订)。
|
|
90
|
+
*
|
|
91
|
+
* **权威来源已核实**(宿主 `@deepseek-ai/dsh-session` `lib/types/types.d.ts`):官方用量挂在
|
|
92
|
+
* `assistant/message` 事件的 `usage?: TokenUsage` 上(`@deepseek-ai/dsh-llm` `lib/types/types.d.ts:160`),
|
|
93
|
+
* 一次模型调用一条。**不需要我们自己推**——事件里带的就是官方算好的。
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ 官方 `TokenUsage` 的计数是**互斥的**(原文:*Counts are DISJOINT*):
|
|
96
|
+
* - `inputTokens` = **未缓存**输入(不含缓存);
|
|
97
|
+
* - 缓存单列 `cacheReadTokens` / `cacheWriteTokens`;
|
|
98
|
+
* - **计费输入 = inputTokens + cacheReadTokens + cacheWriteTokens**;
|
|
99
|
+
* - `totalTokens` = 这一次调用的完整总量(prompt + output)。
|
|
100
|
+
* ⇒ 只取 `inputTokens` 会**漏掉缓存**(大头)⇒ 面板数远小于会话。这就是之前对不上的根因。
|
|
101
|
+
*
|
|
102
|
+
* 取不到(事件不带 usage)返回 undefined(三列留 null,不阻塞链路)。
|
|
103
|
+
*/
|
|
104
|
+
export declare function extractTokenUsage(event: unknown): TokenUsage | undefined;
|
|
105
|
+
export declare function createReconciler({ ctx, logger, store, options, runtime, emit }: ReconcilerDeps): Reconciler;
|